/**
 * Copyright (C) 2010 altuure <altuure [AT] gmail [DOT] com> http://www.altuure.com/projects/yagdao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.altuure.yagdao.jpa;

import com.altuure.yagdao.*;
import com.altuure.yagdao.common.Utils;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.SingularAttribute;
import java.io.Serializable;
import java.util.*;

public class GenericJpaDAO<T, ID extends Serializable> implements GenericDAO<T, ID> {
    private EntityManagerAccessor entityManagerAccessor;
    private Class objectClass;
    private String idProperty;

    public Class<?> getObjectClass() {
        return objectClass;
    }

    public void setObjectClass(Class newObjectClass) {
        this.objectClass = newObjectClass;
    }

    public void setEntityManagerAccessor(EntityManagerAccessor entityManagerAccessor) {
        this.entityManagerAccessor = entityManagerAccessor;

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#save(java.lang.Object)
     */
    public void save(Object entity) {
        getEntityManager().persist(entity);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#load(java.io.Serializable)
     */
    public T load(ID id) {
        return (T) getEntityManager().getReference(objectClass, id);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#getAll()
     */
    public SearchResultList<T> getAll() {
        return getAll(null);

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#getAll(com.altuure.yagdao.DAOPage)
     */
    public SearchResultList<T> getAll(YPage paging) {

        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);

        CriteriaQuery<Object> select = criteriaQuery.select(from);

        return executeQuery(select, paging, criteriaBuilder, from);

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#get(java.io.Serializable)
     */
    public T get(ID id) {

        return (T) getEntityManager().find(objectClass, id);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#fetch(java.io.Serializable)
     */
    public T fetch(ID id) {
        YFetch daoFetch = (YFetch) objectClass.getAnnotation(YFetch.class);
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();

        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);

        CriteriaQuery<Object> select = addFetchStatement(criteriaQuery, daoFetch, from);
        criteriaQuery.where(criteriaBuilder.equal(from.get(getIdProperty()), id));

        TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
        return (T) typedQuery.getSingleResult();
    }

    /**
     * @param criteriaQuery root criteria
     * @param daoFetch fetch properties
     * @param from root
     * @return  created CriteriaQuery
     */
    private CriteriaQuery<Object> addFetchStatement(CriteriaQuery<Object> criteriaQuery, YFetch daoFetch, Root from) {
        CriteriaQuery<Object> select = criteriaQuery.select(from);

        if (daoFetch != null) {
            String[] strings = daoFetch.value();
            for (String s : strings) {
                from.fetch(s);
            }

        }
        return select;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#count()
     */
    public long count() {
        Number count = (Number) getEntityManager().createQuery(
                "select count(*) from " + getObjectClass().getName() + " s ").getSingleResult();

        return count.longValue();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#update(java.lang.Object)
     */
    public void update(Object object) {
        getEntityManager().merge(object);

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#deleteById(java.io.Serializable)
     */
    public void deleteById(ID id) {
        delete(load(id));

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#delete(java.lang.Object)
     */
    public void delete(Object object) {
        getEntityManager().remove(object);

    }

    /**
     * set limit for the query
     * @param query query
     * @param firstResult first result index
     * @param pageSize page size
     */
    public void setLimits(Query query, int firstResult, int pageSize) {
        setLimits(query, new YLimit(firstResult, pageSize));
    }

    /**
     * set limits for query
     * @param query query
     * @param limit page limits
     */
    public void setLimits(Query query, YLimit limit) {
        if (limit == null)
            return;
        if (limit.isPaged())
            query.setFirstResult(limit.getFirstResult());
        if (limit.isPageSized())
            query.setMaxResults(limit.getPageSize());
    }

    public SearchResultList<T> query(Query query, YLimit paging) {
        setLimits(query, paging);
        return new SearchResultList<T>(query.getResultList(), paging);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#fetchAll()
     */
    public SearchResultList<T> fetchAll() {
        return fetchAll(null);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#fetchAll(com.altuure.yagdao.DAOPage)
     */
    public SearchResultList<T> fetchAll(YPage paging) {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);
        YFetch daoFetch = (YFetch) objectClass.getAnnotation(YFetch.class);
        CriteriaQuery<Object> select = addFetchStatement(criteriaQuery, daoFetch, from);
        return executeQuery(select, paging, criteriaBuilder, from);

    }

    /**
     * execute query with given properties
     * @param select root query
     * @param paging paging
     * @param criteriaBuilder root builder
     * @param from parent from
     * @return executed list
     */
    private SearchResultList<T> executeQuery(CriteriaQuery<Object> select, YPage paging,
            CriteriaBuilder criteriaBuilder, Root from) {
        if (paging == null) {
            TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
            return new SearchResultList<T>((List<T>) typedQuery.getResultList());
        }

        setOrders(select, paging, criteriaBuilder, from, new HashMap());
        TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
        setLimits(typedQuery, paging);

        return new SearchResultList<T>((List<T>) typedQuery.getResultList(), paging);
    }

    /**
     * set Orders of given criteria.
     * @param select selct
     * @param paging paging
     * @param criteriaBuilder criteria builder
     * @param from root from
     * @param joins joins map
     */
    public void setOrders(CriteriaQuery<Object> select, YPage paging, CriteriaBuilder criteriaBuilder, Root from,
            Map<String, From> joins) {
        if (paging == null)
            return;
        if (paging.isOrdered()) {
            ArrayList<Order> jpaOrders = new ArrayList<Order>();
            List<YOrder> orders = paging.getOrders();
            for (YOrder daoOrder : orders) {

                Path path = extractPath(from, joins, daoOrder.getName());
                Order jpaOrder = (daoOrder.getSort().equals(YOrder.Sort.ASC)) ? criteriaBuilder.asc(path)
                        : criteriaBuilder.desc(path);
                jpaOrders.add(jpaOrder);
            }
            select.orderBy(jpaOrders.toArray(new Order[0]));

        }
    }

    /**
     * returns the id property for given entity
     * @return id proeprty name
     */
    public String getIdProperty() {
        if (idProperty != null)
            return idProperty;
        Metamodel metamodel = getEntityManager().getMetamodel();
        EntityType entity = metamodel.entity(objectClass);
        Set<SingularAttribute> singularAttributes = entity.getSingularAttributes();
        for (SingularAttribute singularAttribute : singularAttributes) {
            if (singularAttribute.isId())
                idProperty = singularAttribute.getName();
        }
        if (idProperty == null)
            throw new RuntimeException("id field not found");
        return idProperty;
    }

    private EntityManager getEntityManager() {
        return entityManagerAccessor.getEntityManager();
    }

    protected Path extractPath(Root from, Map<String, From> joins, String property) {
        Path path;
        String[] strings = Utils.splitDot(property);

        From join = getLatestJoin(from, joins, strings);
        path = join.get(strings[strings.length - 1]);

        return path;
    }

    /**
     * create latest join from given root crietria from
     * @param from root
     * @param joins join map cache
     * @param strings . splited field array
     * @return get latest join before property
     */
    protected From getLatestJoin(Root from, Map<String, From> joins, String[] strings) {
        // /not implemented yet

        From join = from;
        for (int j = 0; j < strings.length - 1; j++) {
            String propertyName = strings[j];
            String key = keyMaker(strings, j);

            if (joins.containsKey(key)) {
                join = joins.get(key);

            } else {
                if (j == 0) {
                    join = from.join(propertyName);
                } else {
                    join = join.join(propertyName);
                }
                joins.put(key, join);
            }

        }
        return join;
    }

    /**
     * create latest join from given root crietria from
     * @param from root
     * @param fetchs fetch map cache
     * @param strings . splited field array
     * @return get latest fetch
     */
    @SuppressWarnings("rawtypes")
    protected FetchParent getLatestFetch(Root from, Map<String, FetchParent> fetchs, String[] strings) {
        // /not implemented yet

        FetchParent fetch = from;
        for (int j = 0; j < strings.length; j++) {
            String propertyName = strings[j];
            String key = keyMaker(strings, j);

            if (fetchs.containsKey(key)) {
                fetch = fetchs.get(key);

            } else {
                if (j == 0) {
                    fetch = from.fetch(propertyName);
                } else {
                    fetch = fetch.fetch(propertyName);
                }
                fetchs.put(key, fetch);
            }

        }
        return fetch;
    }

    /**
     * create key from seperated key array
     * @param strings key array seperated by .
     * @param j last index of proerty
     * @return key
     */
    protected String keyMaker(String[] strings, int j) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i <= j; i++) {
            String string = strings[i];
            if (i > 0)
                stringBuilder.append(".");
            stringBuilder.append(string);

        }
        return stringBuilder.toString();
    }

    public void detach(Object entity) {
        getEntityManager().detach(entity);

    }

}
